﻿using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using Com.Lancetinc.Evs.Helpers;
using Com.Lancetinc.Evs.Properties;
using NLog;

namespace Com.Lancetinc.Evs.Communications
{
    /// <summary>
    /// 串口通信
    /// </summary>
    public class SerialConnection : IConnection
    {
        #region 创建对象

        /// <summary>
        /// 创建 串口同步通讯 对象
        /// </summary>
        public SerialConnection()
        {
            _serialPort = new SerialPort
            {
                BaudRate = Settings.Default.BaudRate,
                DataBits = 8,
                StopBits = StopBits.One,
                ReadBufferSize = 10240,
                ReadTimeout = 1000,
                WriteTimeout = 1000,
                ReceivedBytesThreshold = 1
            };
            _serialPort.DataReceived += _serialPort_DataReceived;
        }

        #endregion

        public static bool PrintLog { get; set; } = false;

        public int ReadTimeout
        {
            get => _serialPort.ReadTimeout;
            set => _serialPort.ReadTimeout = value;
        }

        public event EventHandler<PacketReceivedEventArgs> PacketReceived;

        /// <summary>
        /// 最后一次收到数据的时间
        /// </summary>
        public DateTime LastReceivedDataTime { get; private set; }

        /// <summary>
        /// 发送数据包
        /// </summary>
        /// <param name="packet"></param>
        public void Send(Packet packet)
        {
            var sendingWhenEnter = _sending;
            lock (this)
            {
                if (sendingWhenEnter)
                {
                    Thread.Sleep(10);
                }

                try
                {
                    _sending = true;
                    SendPacket(packet);
                }
                finally
                {
                    _sending = false;
                }
            }
        }

        /// <summary>
        /// 已联机
        /// </summary>
        public bool IsConnected
        {
            get => _connected && _serialPort.IsOpen;
            set => _connected = value;
        }

        public void Reconnect()
        {
            Connect(PortName);
        }

        /// <summary>
        /// 定时调用此方法，接收串口数据。
        /// </summary>
        private void _serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            DoTryReceiveData();
        }

        private void DoTryReceiveData()
        {
            if (!_serialPort.IsOpen || _serialPort.BytesToRead == 0)
            {
                return;
            }

            lock (_receivedBytes)
            {
                if (!_serialPort.IsOpen || _serialPort.BytesToRead == 0)
                {
                    return;
                }

                try
                {
                    OnReceiveSerialData();
                }
                catch (CommInvalidDataException ex)
                {
                    Logger.Info(PortName + ":CommInvalidDataException:" + ex.Message);
                    _continueReceivedInvalidPacketCount++;

                    //无效的数据包，清空接收到数据
                    _receivedBytes.Clear();

                    //如果连续接收到两个无效的数据包时，抛出异常。
                    if (_continueReceivedInvalidPacketCount > 1)
                    {
                        throw ex;
                    }
                }
            }
        }

        private byte[] BatchReadBytes()
        {
            if (!_serialPort.IsOpen || _serialPort.BytesToRead == 0)
            {
                return Array.Empty<byte>();
            }

            var bytes = new List<byte>();
            while (_serialPort.IsOpen && _serialPort.BytesToRead > 0)
            {
                var cnt = _serialPort.BytesToRead;
                var buffer = new byte[cnt];
                var readCount = _serialPort.Read(buffer, 0, cnt);
                bytes.AddRange(ArrayHelper.SubArray(buffer, 0, readCount));
                Thread.Sleep(10); // 有时候一个命令分多次上来，稍微等下，让命令可以一次全部接收
            }

            return bytes.ToArray();
        }

        protected virtual void OnReceiveSerialData()
        {
            var begin = _receivedBytes.Count > 0;
            var bytes = BatchReadBytes();
            if (bytes.Length == 0)
            {
                return;
            }

            LastReceivedDataTime = DateTime.Now;
            foreach (var b in bytes)
            {
                if (!begin)
                {
                    if (b == (byte)CtrlChars.BEGIN)
                    {
                        begin = true;
                        _receivedBytes.Clear(); // 移除以前数据
                    }

                    continue;
                }

                if (b == (byte)CtrlChars.END)
                {
                    if (_receivedBytes.Count > 0)
                    {
                        if (PrintLog && Logger.IsDebugEnabled)
                        {
                            var logData = string.Join(" ", _receivedBytes.Select(x => x.ToString("X2")));
                            Logger.Debug(PortName + ":received:count=" + _receivedBytes.Count + ",data=" + logData);
                        }

                        var command = (CtrlCmds)_receivedBytes[0];
                        var pktBytes = _receivedBytes.Skip(1).ToArray();
                        var data = Encoding.ASCII.GetString(pktBytes);
                        var pkt = new Packet(command, data)
                        {
                            OrgBytes = pktBytes
                        };
                        OnPacketReceived(pkt);
                    }

                    _receivedBytes.Clear();
                }
                else
                {
                    _receivedBytes.Add(b);
                }
            }
        }

        protected virtual void SendPacket(Packet packet)
        {
            var buf = packet.Serialize();
            if (PrintLog && Logger.IsDebugEnabled)
            {
                Logger.Debug(PortName + ":send:" + packet + $" ({Encoding.ASCII.GetString(buf)})");
            }

            _serialPort.Write(buf, 0, buf.Length);
        }

        protected void OnPacketReceived(Packet packet)
        {
            if (PrintLog && Logger.IsDebugEnabled)
            {
                Logger.Debug(PortName + ":recv:" + packet);
            }

            if (!_connected && packet.Command == CtrlCmds.CONNECT)
            {
                _connected = true;
            }

            if (_connected)
            {
                PacketReceived?.Invoke(this, new PacketReceivedEventArgs(packet));
            }
        }

        #region 变量

        /// <summary>
        /// 接收到字节数据
        /// </summary>
        private readonly List<byte> _receivedBytes = new();

        /// <summary>
        /// 串口通讯
        /// </summary>
        private readonly SerialPort _serialPort;

        /// <summary>
        /// 连续接收到无效数据包的个数
        /// </summary>
        private int _continueReceivedInvalidPacketCount;

        /// <summary>
        /// 已连接？
        /// </summary>
        private bool _connected;

        private volatile bool _sending;

        /// <summary>
        /// 日志记录
        /// </summary>
        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

        /// <summary>
        /// 当前使用的端口名称
        /// </summary>
        public string PortName { get; private set; }

        #endregion

        #region 辅助方法

        /// <summary>
        /// 打开连接
        /// </summary>
        /// <param name="portName">端口名称</param>
        public virtual void Connect(string portName)
        {
            Close();
            Logger.Info(portName + " to open");
            _serialPort.PortName = portName;
            _serialPort.Open();
            PortName = portName;
        }

        /// <summary>
        /// 关闭连接
        /// </summary>
        public void Close()
        {
            if (_serialPort.IsOpen)
            {
                try
                {
                    Logger.Info(PortName + ": close");
                    _serialPort.Close();
                }
                catch
                {
                    //
                }
            }

            _connected = false;
            _receivedBytes.Clear();
        }

        /// <summary>
        /// 取得可用的端口。
        /// 如果指定默认端口，且默认端口可用，将默认端口放在所有可用端口的最前面。
        /// </summary>
        /// <param name="defaultPort">默认端口</param>
        /// <returns></returns>
        public static string[] GetPortNames(string defaultPort)
        {
            // 可以使用的端口
            var portNames = SerialPort.GetPortNames();

            if (portNames.Length == 0)
            {
                return portNames;
            }

            // 将最后一次成功联机时的端口移到可用端口的最前面
            if (!string.IsNullOrEmpty(defaultPort)
                && Array.IndexOf(portNames, defaultPort) >= 0)
            {
                IList<string> list = portNames.Where(x => x != defaultPort).ToList();
                list.Insert(0, defaultPort);
                portNames = list.ToArray();
            }
            else
            {
                // Silicon Labs的串口排在前面
                TryInitPriority();
                if (_priorityPorts is { Count: > 0 })
                {
                    var oldPortNames = new List<string>(portNames);
                    var newPortNames = new List<string>();
                    foreach (var p in _priorityPorts)
                    {
                        if (!oldPortNames.Contains(p))
                            continue;

                        newPortNames.Add(p);
                        oldPortNames.Remove(p);
                    }
                    newPortNames.AddRange(oldPortNames);
                    portNames = newPortNames.ToArray();
                }

            }

            return portNames;
        }

        private static HashSet<string> _priorityPorts = null;
        private static void TryInitPriority()
        {
            if (_priorityPorts != null)
                return;

            Dictionary<string, SerialPortInfo> dic;
            try
            {
                dic = WinSystemHelper.GetSerialPortInfo();
            }
            catch (Exception e)
            {
                Logger.Error(e);
                return;
            }

            if (dic.Count == 0)
                return;

            var list = new List<string>();
            foreach (var portName in dic.Keys)
            {
                var info = dic[portName];
                if (info.DeviceName != null && info.DeviceName.Contains("CP210x"))
                {
                    list.Add(portName);
                }
            }
            if (list.Count == 0)
                return;

            _priorityPorts = list.ToHashSet();
        }

        #endregion
    }
}